package com.google.code.sms.dao;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;

import org.springframework.jdbc.core.simple.ParameterizedRowMapper;

import com.google.code.sms.Logger;
import com.google.code.sms.domain.User;
import com.google.code.sms.util.StringUtil;

/**
 * Provides user-related database services.
 *
 * @author Sindre Mehus
 */
public class UserDao extends AbstractDao {

    private static final Logger LOG = Logger.getLogger("UserDao");
    private static final String USER_COLUMNS = "username, password, bytes_streamed, bytes_downloaded";
    private static final Integer ROLE_ID_ADMIN = 1;
    private static final Integer ROLE_ID_DOWNLOAD = 2;
    private static final Integer ROLE_ID_PLAYLIST = 3;
    private static final Integer ROLE_ID_SETTINGS = 4;
    private static final Integer ROLE_ID_STREAM = 5;
    private UserRowMapper userRowMapper = new UserRowMapper();

    /**
     * Returns the user with the given username.
     *
     * @param username The username used when logging in.
     * @return The user, or <code>null</code> if not found.
     */
    public User getUserByName(String username) {
        String sql = "select " + USER_COLUMNS + " from user where username=?";
        return queryOne(sql, userRowMapper, username);
    }

    /**
     * Returns all users.
     *
     * @return Possibly empty array of all users.
     */
    public List<User> getAllUsers() {
        String sql = "select " + USER_COLUMNS + " from user";
        return query(sql, userRowMapper);
    }

    /**
     * Creates a new user.
     *
     * @param user The user to create.
     */
    public void createUser(User user) {
        String sql = "insert into user (" + USER_COLUMNS + ") values (" + questionMarks(USER_COLUMNS) + ')';
        update(sql, user.getUsername(), encrypt(user.getPassword()), user.getBytesStreamed(), user.getBytesDownloaded());
        writeRoles(user);
    }

    /**
     * Deletes the user with the given username.
     *
     * @param username The username.
     */
    public void deleteUser(String username) {
        if (User.USERNAME_ADMIN.equals(username)) {
            throw new IllegalArgumentException("Can't delete admin user.");
        }

        String sql = "delete from user_role where username=?";
        update(sql, username);

        sql = "delete from user where username=?";
        update(sql, username);
    }

    /**
     * Updates the given user.
     *
     * @param user The user to update.
     */
    public void updateUser(User user) {
        String sql = "update user set password=?, bytes_streamed=?, bytes_downloaded=?"
                + "where username=?";
        getJdbcTemplate().update(sql, new Object[]{encrypt(user.getPassword()), user.getBytesStreamed(), user.getBytesDownloaded(), user.getUsername()});
        writeRoles(user);
    }

    /**
     * Returns the name of the roles for the given user.
     *
     * @param username The user name.
     * @return Roles the user is granted.
     */
    public String[] getRolesForUser(String username) {
        String sql = "select r.name from role r, user_role ur "
                + "where ur.username=? and ur.role_id=r.id";
        List<?> roles = getJdbcTemplate().queryForList(sql, new Object[]{username}, String.class);
        String[] result = new String[roles.size()];
        for (int i = 0; i < result.length; i++) {
            result[i] = (String) roles.get(i);
        }
        return result;
    }

    private static String encrypt(String s) {
        if (s == null) {
            return null;
        }
        try {
            return "enc:" + StringUtil.utf8HexEncode(s);
        } catch (Exception e) {
            return s;
        }
    }

    private static String decrypt(String s) {
        if (s == null) {
            return null;
        }
        if (!s.startsWith("enc:")) {
            return s;
        }
        try {
            return StringUtil.utf8HexDecode(s.substring(4));
        } catch (Exception e) {
            return s;
        }
    }

    private void readRoles(User user) {
        synchronized (user.getUsername().intern()) {
            String sql = "select role_id from user_role where username=?";
            List<?> roles = getJdbcTemplate().queryForList(sql, new Object[]{user.getUsername()}, Integer.class);
            for (Object role : roles) {
                if (ROLE_ID_ADMIN.equals(role)) {
                    user.setAdminRole(true);
                } else if (ROLE_ID_DOWNLOAD.equals(role)) {
                    user.setDownloadRole(true);
                } else if (ROLE_ID_PLAYLIST.equals(role)) {
                    user.setPlaylistRole(true);
                } else if (ROLE_ID_STREAM.equals(role)) {
                    user.setStreamRole(true);
                } else if (ROLE_ID_SETTINGS.equals(role)) {
                    user.setSettingsRole(true);
                } else {
                    LOG.warn("Unknown role: '" + role + '\'');
                }
            }
        }
    }

    private void writeRoles(User user) {
        synchronized (user.getUsername().intern()) {
            String sql = "delete from user_role where username=?";
            getJdbcTemplate().update(sql, new Object[]{user.getUsername()});
            sql = "insert into user_role (username, role_id) values(?, ?)";
            if (user.isAdminRole()) {
                getJdbcTemplate().update(sql, new Object[]{user.getUsername(), ROLE_ID_ADMIN});
            }
            if (user.isDownloadRole()) {
                getJdbcTemplate().update(sql, new Object[]{user.getUsername(), ROLE_ID_DOWNLOAD});
            }
            if (user.isPlaylistRole()) {
                getJdbcTemplate().update(sql, new Object[]{user.getUsername(), ROLE_ID_PLAYLIST});
            }
            if (user.isStreamRole()) {
                getJdbcTemplate().update(sql, new Object[]{user.getUsername(), ROLE_ID_STREAM});
            }
            if (user.isSettingsRole()) {
                getJdbcTemplate().update(sql, new Object[]{user.getUsername(), ROLE_ID_SETTINGS});
            }
        }
    }

    private class UserRowMapper implements ParameterizedRowMapper<User> {

        @Override
        public User mapRow(ResultSet rs, int rowNum) throws SQLException {
            User user = new User(rs.getString(1), decrypt(rs.getString(2)), rs.getLong(3), rs.getLong(4));
            readRoles(user);
            return user;
        }
    }
}
